﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Xml.Linq;

namespace LibXML
{
	public class XMLManager
	{
		public string FilePath { get; private set; }
		public IEnumerable<State> States { get; set; }
		public IEnumerable<Transition> Transitions { get; set; }
		public IEnumerable<Note> Notes { get; set; }
		public string MachineType { get; set; }

		private XDocument document;

		/// <summary>
		/// Creates a XML manager for saving purposes
		/// </summary>
		public XMLManager()
		{
			MachineType = "turing";
			States = new List<State>();
			Transitions = new List<Transition>();
			Notes = new List<Note>();
		}

		/// <summary>
		/// Create a XML manager from the specified XML path
		/// </summary>
		/// <param name="path">The path to the XML file to load</param>
		public XMLManager(string path)
		{
			ValidateFile(path);
			LoadMachineType();
			LoadStates();
			LoadTransitions();
			LoadNotes();
		}

		/// <summary>
		/// Attempt to save the manager to the file path that is specified
		/// </summary>
		/// <param name="path">The path to the file to save the manager to</param>
		public void SaveManager(string path)
		{
			// Update the file path
			if (!String.IsNullOrEmpty(path))
			{
				FilePath = Path.GetFullPath(path);

				// Create the document
				document = new XDocument(new XDeclaration("1.0", "utf-8", "no"), new XElement("structure"));

				// Add the structure elements
				document.Root.Add(new XElement("type", MachineType));
				document.Root.Add(new XElement("automaton"));

				// Add the automaton elements
				IEnumerable<object> elements = PreappendComment(SaveStates().Cast<object>(), "The list of states");
				elements = elements.Union(PreappendComment(SaveTransitions().Cast<object>(), "The list of transitions"));
				elements = elements.Union(PreappendComment(SaveNotes().Cast<object>(), "The list of notes"));
				foreach (object element in elements)
				{
					document.Root.Element("automaton").Add(element);
				}

				// Save the document
				document.Save(FilePath);
			}
		}

		/// <summary>
		/// Preappend a comment to a collection of elements if there is at least one element
		/// </summary>
		/// <param name="elements">The colleciton of elements to preappend a comment to</param>
		/// <param name="comment">The comment to preappend</param>
		/// <returns>A collection of the elements with the comment preappended</returns>
		private IEnumerable<object> PreappendComment(IEnumerable<object> elements, string comment)
		{
			if (elements.Count() > 0)
			{
				elements = new object[] { new XComment(comment) }.Concat(elements).ToList();
			}
			return elements.Cast<object>();
		}

		/// <summary>
		/// Validate the file before any information is loaded
		/// </summary>
		/// <param name="path">The path to the file to validate</param>
		private void ValidateFile(string path)
		{
			// Check if the file exists
			FilePath = Path.GetFullPath(path);
			if (!File.Exists(FilePath))
				throw new FileNotFoundException("The specified file was not found and could not be loaded.");

			// Check if its a valid XML file
			try
			{
				document = XDocument.Load(FilePath);
			}
			catch
			{
				throw new NotSupportedException("This file is not a supported XML file.");
			}

			// Check if its a valid JFLAP file
			if (document.Root == null || document.Root.Name.LocalName != "structure")
				throw new NotSupportedException("This file is not a supported JFLAP file.");
		}

		/// <summary>
		/// Try to read the specific element name from the given element and return its value.
		/// </summary>
		/// <param name="element">The element to scan the children elements of.</param>
		/// <param name="elementName">The name of the child element to find.</param>
		/// <returns>The value of the child element if it was found, otherwise its empty.</returns>
		private string TryRead(XElement element, string elementName)
		{
			return (element.Element(elementName) == null) ? string.Empty : element.Element(elementName).Value;
		}

		/// <summary>
		/// Retrieve the machine type from the file
		/// </summary>
		private void LoadMachineType()
		{
			MachineType = TryRead(document.Root, "type");
		}

		/// <summary>
		/// Retrieve the states from the file
		/// </summary>
		private void LoadStates()
		{
			States = document.Root.Element("automaton").Elements("state").Select(element => new State()
			{
				Name = element.Attribute("name").Value,
				Id = element.Attribute("id").Value,
				X = String.IsNullOrEmpty(TryRead(element, "x")) ? 0 : Double.Parse(TryRead(element, "x")),
				Y = String.IsNullOrEmpty(TryRead(element, "y")) ? 0 : Double.Parse(TryRead(element, "y")),
				Initial = element.Element("initial") != null,
				Final = element.Element("final") != null,
				Label = TryRead(element, "label"),
				ShowLabel = element.Element("showlabel") != null
			});
		}

		/// <summary>
		/// Save the states to the file
		/// </summary>
		/// <returns>A collection of states to save to the file</returns>
		private IEnumerable<XElement> SaveStates()
		{
			return States.Select(state => GenerateStateXML(state));
		}

		/// <summary>
		/// Generate XML from a state
		/// </summary>
		/// <param name="state">The state to generate XML from</param>
		/// <returns>The XML that was generated for the state</returns>
		private XElement GenerateStateXML(State state)
		{
			// Basic info
			XElement retValue = new XElement("state",
				new XAttribute("id", state.Id),
				new XAttribute("name", state.Name),
				new XElement("x", state.X),
				new XElement("y", state.Y)
			);

			// Initial/Final/Label
			if (state.Initial)
				retValue.Add(new XElement("initial"));
			if (state.Final)
				retValue.Add(new XElement("final"));
			if (state.ShowLabel)
				retValue.Add(new XElement("showlabel"));
			if (!String.IsNullOrEmpty(state.Label))
				retValue.Add(new XElement("label", state.Label));

			return retValue;
		}

		/// <summary>
		/// Retrieve the transitions from the file
		/// </summary>
		private void LoadTransitions()
		{
			Transitions = document.Root.Element("automaton").Elements("transition").Select(element => new Transition()
			{
				From = TryRead(element, "from"),
				To = TryRead(element, "to"),
				Read = TryRead(element, "read"),
				Write = TryRead(element, "write"),
				Move = TryRead(element, "move"),
				Push = TryRead(element, "push"),
				Pop = TryRead(element, "pop"),
				X = String.IsNullOrEmpty(TryRead(element, "x")) ? 0 : Double.Parse(TryRead(element, "x")),
				Y = String.IsNullOrEmpty(TryRead(element, "y")) ? 0 : Double.Parse(TryRead(element, "y")),
				IsPathVisible = element.Element("invisible") == null
			});
		}

		/// <summary>
		/// Save the transitions to the file
		/// </summary>
		/// <returns>A collection of transitions to save to the file</returns>
		private IEnumerable<XElement> SaveTransitions()
		{
			return Transitions.Select(transition => GenerateTransitionXML(transition));
		}

		/// <summary>
		/// Generate XML from a transition
		/// </summary>
		/// <param name="transition">The transition to generate XML from</param>
		/// <returns>The XML that was generated for the transition</returns>
		private XElement GenerateTransitionXML(Transition transition)
		{
			// Basic info
			XElement retValue = new XElement("transition",
				new XElement("from", transition.From),
				new XElement("to", transition.To),
				new XElement("read", transition.Read)
			);

			// Transition info
			if(MachineType == "turing")
			{
				retValue.Add(new XElement("write", transition.Write));
				retValue.Add(new XElement("move", transition.Move));
			}
			else if (MachineType == "pda")
			{
				retValue.Add(new XElement("push", transition.Push));
				retValue.Add(new XElement("pop", transition.Pop));
			}
			if (transition.X != 0)
				retValue.Add(new XElement("x", transition.X));
			if (transition.Y != 0)
				retValue.Add(new XElement("y", transition.Y));
			if (!transition.IsPathVisible)
				retValue.Add(new XElement("invisible"));

			return retValue;
		}

		/// <summary>
		/// Retrieve the notes from the file
		/// </summary>
		private void LoadNotes()
		{
			Notes = document.Root.Element("automaton").Elements("note").Select(element => new Note()
			{
				Text = TryRead(element, "text"),
				X = String.IsNullOrEmpty(TryRead(element, "x")) ? 0 : Double.Parse(TryRead(element, "x")),
				Y = String.IsNullOrEmpty(TryRead(element, "y")) ? 0 : Double.Parse(TryRead(element, "y"))
			});
		}

		/// <summary>
		/// Save the notes to the file
		/// </summary>
		/// <returns>A collection of notes to save to the file</returns>
		private IEnumerable<XElement> SaveNotes()
		{
			return Notes.Select(note => GenerateNoteXML(note));
		}

		/// <summary>
		/// Genereate XML from a note
		/// </summary>
		/// <param name="note">The note to generate XML from</param>
		/// <returns>The XML that was generated for the note</returns>
		private XElement GenerateNoteXML(Note note)
		{
			return new XElement("note",
				new XElement("text", note.Text),
				new XElement("x", note.X),
				new XElement("y", note.Y)
			);
		}
	}
}
